/**
 * Catalogo
 * Catalogo dos arquivos sincronizados
 * Author:Nuno 
 */
package catalogo;

import java.io.*;
import java.text.DateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;

import org.w3c.dom.*;


public class Catalogo 
{
	public static final String STATUS_REQUEST_FILE = new String("REQUEST_FILE");
	public static final String STATUS_FILE_REQUESTED = new String("FILE_REQUESTED");	
	public static final String STATUS_REQUEST = new String("REQUEST_TS");
	public static final String STATUS_DELETED = new String("DELETED");
	public static final String STATUS_OK = new String("OK");
	private String nomeCatalogo;
	private String rootDir;
	private Document docXml;
	private Hashtable<String,ArquivoCatalogo> arquivos;

	/**
	 * constructor
	 * Carrega o catalogo em XML
	 */
	public Catalogo(String rootDir)
	{
		super();
		this.rootDir = rootDir;
		this.nomeCatalogo = rootDir+File.separator+".catalogo.xml";
		this.arquivos = new Hashtable<String,ArquivoCatalogo>(); 
		this.importXML();
		this.carregaArquivos();
	}

	/*
	 * Getters and Setters
	 */	
	public String getNomeCatalogo() {
		return nomeCatalogo;
	}

	public void setNomeCatalogo(String rootDir) {
		this.nomeCatalogo = rootDir+File.separator+".catalogo.xml";
	}

	public String getrootDir() {
		return rootDir;
	}

	public void setrootDir(String rootDir) {
		this.rootDir = rootDir;
	}	

	public Document getDocXml() {
		return docXml;
	}

	public void setDocXml(Document docXml) {
		this.docXml = docXml;
	}

	public Hashtable<String, ArquivoCatalogo> getArquivos() {
		return arquivos;
	}

	public void setArquivos(Hashtable<String, ArquivoCatalogo> arquivos) {
		this.arquivos = arquivos;
	}	

	// Metodos
	//Importa arquivo XML, se nao existir cria-o
	public void importXML()
	{		
		try {
			DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
			this.docXml = docBuilder.parse (new File(nomeCatalogo));
			// normalize text representation
			this.docXml.getDocumentElement ().normalize ();
		} catch (Exception e) {
			try
			{
				// Stream to write file
				FileOutputStream fout;		

				// Open an output stream
				fout = new FileOutputStream (nomeCatalogo);

				// Print a line of text
				new PrintStream(fout).println (" ");

				// Close our output stream
				fout.close();		

				DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
				DocumentBuilder parser = fact.newDocumentBuilder();
				Document doc = parser.newDocument();

				Element root = doc.createElement("catalogo");
				doc.appendChild(root);	
				TransformerFactory transformerFactory = TransformerFactory.newInstance();
				Transformer transformer = transformerFactory.newTransformer();
				DOMSource source = new DOMSource(doc);
				StreamResult result =  new StreamResult(nomeCatalogo);
				transformer.transform(source, result);
				this.docXml=doc;
			}
			// Catches any error conditions
			catch (Exception es)
			{			
				System.err.println ("Impossivel criar o arquivo "+this.nomeCatalogo);
				System.exit(-1);
			}

		}		
	}

	// Cria o documento XML, grava-o em disco e guarda-o no docXML
	public void exportXML() 
	{
		try {
			DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
			DocumentBuilder parser = fact.newDocumentBuilder();
			Document doc = parser.newDocument();

			Element root = doc.createElement("catalogo");
			doc.appendChild(root);

			for (Enumeration<ArquivoCatalogo> arqs = this.arquivos.elements() ; arqs.hasMoreElements() ;) 
			{
				ArquivoCatalogo a = this.arquivos.get(arqs.nextElement().getNome()); 				
				Element arquivo = doc.createElement("arquivo");
				arquivo.setAttribute("nome", a.getNome());
				root.appendChild(arquivo);

				Element data = doc.createElement("data");	
				DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
				String data_str = df.format(a.getData());
				data.appendChild(doc.createTextNode(data_str));
				arquivo.appendChild(data);

				Element timestamp = doc.createElement("timestamp");	    
				timestamp.appendChild(doc.createTextNode(Integer.toString(a.getTimestamp())));
				arquivo.appendChild(timestamp);

				Element status = doc.createElement("status");	    
				status.appendChild(doc.createTextNode(a.getStatus()));
				arquivo.appendChild(status);		    	  
			}


			this.docXml = doc;
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result =  new StreamResult(nomeCatalogo);
			transformer.transform(source, result);	        


		} catch (Exception ex) {
			System.err.println("+============================+");
			System.err.println("|        XML Error           |");
			System.err.println("+============================+");
			System.err.println(ex.getClass());
			System.err.println(ex.getMessage());
			System.err.println("+============================+");
		}
	}

	/**
	 * Le o catalogo XML
	 */
	public void carregaArquivos()
	{
		Element nohRoot = docXml.getDocumentElement();
		//System.out.println("ROOT:"+nohRoot.getNodeName());
		int total = nohRoot.getElementsByTagName("arquivo").getLength();
		//System.out.println("TOTAL:"+total);

		for (int i = 0; i < total; i++)
		{
			Element nohArq = (Element) nohRoot.getElementsByTagName("arquivo").item(i);

			ArquivoCatalogo ac = new ArquivoCatalogo();

			ac.setNome(nohArq.getAttribute("nome"));

			DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
			Date dt = null;
			try {
				dt = df.parse(nohArq.getElementsByTagName("data").item(0).getTextContent());
			} catch (ParseException e) {
				e.printStackTrace();
			}
			ac.setData(dt);

			ac.setTimestamp(new Integer(nohArq.getElementsByTagName("timestamp").item(0).getTextContent()));
			ac.setStatus(nohArq.getElementsByTagName("status").item(0).getTextContent());
			arquivos.put(ac.getNome(), ac);
		}
	}

	public void updateArquivo(ArquivoCatalogo a)
	{
		arquivos.put(a.getNome(), a);
		this.exportXML();
	}
	
	public void setStatus(String fileName, String status)
	{
		if (this.arquivos.containsKey(fileName))
		{
			ArquivoCatalogo arq = this.arquivos.get(fileName);
			arq.setStatus(status);
			arquivos.put(fileName, arq); // is it necessary to put again?
			this.exportXML();
		}
	}

	public Vector<ArquivoCatalogo> diffArquivo(Hashtable<String,ArquivoCatalogo> arqs_cmp)
	{
		Vector<ArquivoCatalogo> resultado = new Vector<ArquivoCatalogo>();
		for (Enumeration<ArquivoCatalogo> arqs = arqs_cmp.elements() ; arqs.hasMoreElements() ;) 
		{
			ArquivoCatalogo arq_remoto = arqs.nextElement();
			if (this.arquivos.containsKey(arq_remoto.getNome()))
			{			
				switch (this.arquivos.get(arq_remoto.getNome()).diffArquivo(arq_remoto))
				{
				case ArquivoCatalogo.OLD_TIMESTAMP:
					resultado.add(arq_remoto);
					System.out.println("Arquivo: "+arq_remoto.getNome()+" tem um timestamp maior.");
					break;
				case ArquivoCatalogo.DELETED_FILE:
					resultado.add(arq_remoto);
					this.setStatus(arq_remoto.getNome(), STATUS_DELETED);
					System.out.println("Arquivo "+arq_remoto.getNome()+" foi apagado remotamente.");				
					break;
				}
			}
			else 
			{   
				if (!arq_remoto.getStatus().equals(STATUS_DELETED) && !arq_remoto.getStatus().equals(STATUS_FILE_REQUESTED))
				{
					// new remote file					
					ArquivoCatalogo novo = new ArquivoCatalogo(arq_remoto.getNome(), 
							Catalogo.FormatDate(0), 
							arq_remoto.getTimestamp(), STATUS_REQUEST_FILE);
					this.updateArquivo(novo);					
					resultado.add(arq_remoto);
					System.out.println("Arquivo "+arq_remoto.getNome()+" nao existe no catalogo local.");
				}		
			}
		}
		return resultado;
	}

	public int get_timestamp(String nome_arquivo)
	{
		if (this.arquivos.containsKey(nome_arquivo))
		{
			return this.arquivos.get(nome_arquivo).getTimestamp();
		}
		else
			return -1;	
	}

	public void set_timestamp(String nome_arquivo,int timestamp) 
	{
		File novo = new File(this.rootDir, nome_arquivo);
		Date data_formatted = null;
		data_formatted = FormatDate(novo.lastModified());

		if (this.arquivos.containsKey(nome_arquivo))
		{
			ArquivoCatalogo arq = this.arquivos.get(nome_arquivo);
			if (arq.getTimestamp() < timestamp)
			{
				arq.setTimestamp(timestamp);
				arq.setData(data_formatted);
			}
			arq.setStatus(STATUS_OK);
		}	
		this.exportXML();
	}

	/*
	 *  diffFiles: compara os arquivos locais com o catalogo
	 */
	public Vector<ArquivoCatalogo> diffFiles(File[] ficheiros) 
	{
		Vector<ArquivoCatalogo> result = new Vector<ArquivoCatalogo>();
		Vector<String> checkFilesdeleted =  new Vector<String>();
		//Check for new files or modifications
		for (int i=0;i<ficheiros.length;i++)
		{
			Date dateFormatted = FormatDate(ficheiros[i].lastModified());
			checkFilesdeleted.add(ficheiros[i].getName());
			if (this.arquivos.containsKey(ficheiros[i].getName()))
			{
				// test if the local file was modified
				if (this.arquivos.get(ficheiros[i].getName()).getData().before(dateFormatted))
				{
					ArquivoCatalogo novo = new ArquivoCatalogo(ficheiros[i].getName(),
							dateFormatted,
							this.arquivos.get(ficheiros[i].getName()).getTimestamp(),
							STATUS_REQUEST);
					result.add(novo);
					this.updateArquivo(novo);
					System.out.println("Arquivo "+novo.getNome()+" foi modificado localmente.");
				}	
			}
			else  // the local file is new; it's not in the catalog: add to the catalog and set STATUS_REQUEST
			{
				ArquivoCatalogo novo = new ArquivoCatalogo(ficheiros[i].getName(),
						dateFormatted,
						-1,
						STATUS_REQUEST);
				result.add(novo);
				this.updateArquivo(novo);
				System.out.println("Arquivo "+novo.getNome()+" nao existe no catalogo.");
			}
		}
		// End Check for new files and modifications
		// Check for deleted files
		for (Enumeration<ArquivoCatalogo> arqs = this.arquivos.elements() ; arqs.hasMoreElements() ;) 
		{
			ArquivoCatalogo a = arqs.nextElement();
			if (!checkFilesdeleted.contains(a.getNome())&&(a.getStatus().equals(STATUS_OK)))
			{
				a.setStatus(STATUS_DELETED);
				//result.add(a);
				System.out.println("Arquivo "+a.getNome()+" deixou de existir no diretorio local.");
			}
		}
		this.exportXML();
		return result; 
	}

	public static Date FormatDate(long date)
	{	
		String dateStrFormatted = new String();
		DateFormat df = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
		Date date_file = new Date (date);
		dateStrFormatted = df.format(date_file);	
		Date dateFormatted = null;
		try {
			dateFormatted = df.parse(dateStrFormatted);
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return dateFormatted;
	}
}
